Ištirkite JavaScript dekoratorius: galingą metaprogramavimo funkciją, skirtą metaduomenims pridėti ir AOP šablonams įgyvendinti. Sužinokite, kaip pagerinti kodo pakartotinį naudojimą, skaitomumą ir priežiūrą pasitelkiant praktinius pavyzdžius.
JavaScript Dekoratoriai: Metaduomenų Programavimas ir AOP Šablonai
JavaScript dekoratoriai yra galinga ir išraiškinga metaprogramavimo funkcija, leidžianti modifikuoti arba pagerinti klasių, metodų, savybių ir parametrų elgseną deklaratyviu ir pakartotinai naudojamu būdu. Jie suteikia glaustą sintaksę metaduomenims pridėti ir į aspektus orientuoto programavimo (AOP) principams įgyvendinti, gerinant kodo pakartotinį naudojimą, skaitomumą ir priežiūrą. Šiame išsamiame vadove nuodugniai išnagrinėsime JavaScript dekoratorius, apžvelgsime jų sintaksę, naudojimą ir pritaikymą įvairiuose scenarijuose. Nors oficialiai tai vis dar besivystantis pasiūlymas, dekoratoriai yra plačiai pritaikyti, ypač tokiose sistemose kaip „Angular“ ir „NestJS“, o jų poveikis JavaScript kūrimui yra neabejotinas.
Kas yra JavaScript Dekoratoriai?
Dekoratoriai yra specialus deklaracijos tipas, kurį galima pridėti prie klasės deklaracijos, metodo, prieigos metodo (accessor), savybės ar parametro. Jie naudoja @expression formą, kur expression turi būti funkcija, kuri bus iškviesta vykdymo metu su informacija apie dekoruotą deklaraciją. Iš esmės, dekoratoriai veikia kaip funkcijos, kurios apgaubia arba modifikuoja dekoruotą elementą, leisdamos pridėti papildomą funkcionalumą ar metaduomenis, tiesiogiai nekeičiant originalaus kodo.
Galvokite apie dekoratorius kaip apie anotacijas ar žymes, kurias galima pridėti prie kodo elementų. Šios žymės gali būti apdorojamos vykdymo metu, atliekant įvairias užduotis, tokias kaip registravimas, validavimas, autorizacija ar priklausomybių injekcija. Dekoratoriai skatina švaresnę ir modularesnę kodo struktūrą, atskiriant atsakomybes ir mažinant pasikartojančio kodo kiekį.
Dekoratorių Naudojimo Privalumai
- Geresnis Kodo Pakartotinis Panaudojimas: Dekoratoriai leidžia inkapsuliuoti bendrą elgseną į pakartotinai naudojamus komponentus, kuriuos galima taikyti kelioms programos dalims. Tai sumažina kodo dubliavimą ir skatina nuoseklumą.
- Pagerintas Skaitomumas: Atskyrus kryžminius aspektus į dekoratorius, jūsų pagrindinė logika tampa švaresnė ir lengviau suprantama. Dekoratoriai suteikia deklaratyvų būdą išreikšti papildomą elgseną, todėl kodas tampa labiau save dokumentuojantis.
- Padidintas Priežiūros Paprastumas: Dekoratoriai skatina moduliškumą ir atsakomybių atskyrimą, todėl lengviau modifikuoti ar išplėsti programą, nepaveikiant kitų kodo bazės dalių. Tai sumažina klaidų atsiradimo riziką ir supaprastina priežiūros procesą.
- Į Aspektus Orientuotas Programavimas (AOP): Dekoratoriai leidžia įgyvendinti AOP principus, suteikdami galimybę įterpti elgseną į esamą kodą, nekeičiant jo šaltinio kodo. Tai ypač naudinga tvarkant kryžminius aspektus, tokius kaip registravimas, saugumas ir transakcijų valdymas.
Dekoratorių Tipai
JavaScript dekoratoriai gali būti taikomi skirtingų tipų deklaracijoms, kurių kiekviena turi savo specifinį tikslą ir sintaksę:
Klasių Dekoratoriai
Klasių dekoratoriai taikomi klasės konstruktoriui ir gali būti naudojami klasės apibrėžimui modifikuoti arba metaduomenims pridėti. Klasės dekoratorius kaip vienintelį argumentą gauna klasės konstruktorių.
Pavyzdys: Metaduomenų pridėjimas klasei.
function Component(options: { selector: string, template: string }) {
return function (constructor: T) {
return class extends constructor {
selector = options.selector;
template = options.template;
}
}
}
@Component({ selector: 'my-component', template: 'Hello' })
class MyComponent {
constructor() {
// ...
}
}
console.log(new MyComponent().selector); // Output: my-component
Šiame pavyzdyje Component dekoratorius prideda selector ir template savybes prie MyComponent klasės, leisdamas deklaratyviai konfigūruoti komponento metaduomenis. Tai panašu į tai, kaip apibrėžiami „Angular“ komponentai.
Metodų Dekoratoriai
Metodų dekoratoriai taikomi klasės metodams ir gali būti naudojami metodo elgsenai modifikuoti arba metaduomenims pridėti. Metodo dekoratorius gauna tris argumentus:
- Taikinio objektą (arba klasės prototipą, arba klasės konstruktorių, priklausomai nuo to, ar metodas yra statinis).
- Metodo pavadinimą.
- Metodo savybių aprašą (property descriptor).
Pavyzdys: Metodų iškvietimų registravimas.
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned: ${result}`);
return result;
}
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number) {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Output: Calling add with arguments: [2,3]
// add returned: 5
Šiame pavyzdyje Log dekoratorius registruoja metodo iškvietimą ir jo argumentus prieš vykdant originalų metodą, o po įvykdymo registruoja grąžinamąją vertę. Tai paprastas pavyzdys, kaip dekoratoriai gali būti naudojami registravimo ar audito funkcionalumui įgyvendinti, nekeičiant pagrindinės metodo logikos.
Savybių Dekoratoriai
Savybių dekoratoriai taikomi klasės savybėms ir gali būti naudojami savybės elgsenai modifikuoti arba metaduomenims pridėti. Savybės dekoratorius gauna du argumentus:
- Taikinio objektą (arba klasės prototipą, arba klasės konstruktorių, priklausomai nuo to, ar savybė yra statinė).
- Savybės pavadinimą.
Pavyzdys: Savybių verčių validavimas.
function Validate(target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newVal: any) {
if (typeof newVal !== 'number' || newVal < 0) {
throw new Error(`Invalid value for ${propertyKey}. Must be a non-negative number.`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Product {
@Validate
price: number;
constructor(price: number) {
this.price = price;
}
}
const product = new Product(10);
console.log(product.price); // Output: 10
try {
product.price = -5; // Throws an error
} catch (e) {
console.error(e.message);
}
Šiame pavyzdyje Validate dekoratorius patikrina price savybę, užtikrindamas, kad ji būtų neneigiamas skaičius. Jei priskiriama netinkama reikšmė, išmetama klaida. Tai paprastas pavyzdys, kaip dekoratoriai gali būti naudojami duomenų validavimui įgyvendinti.
Parametrų Dekoratoriai
Parametrų dekoratoriai taikomi metodo parametrams ir gali būti naudojami metaduomenims pridėti arba parametro elgsenai modifikuoti. Parametro dekoratorius gauna tris argumentus:
- Taikinio objektą (arba klasės prototipą, arba klasės konstruktorių, priklausomai nuo to, ar metodas yra statinis).
- Metodo pavadinimą.
- Parametro indeksą metodo parametrų sąraše.
Pavyzdys: Priklausomybių injektavimas.
import 'reflect-metadata';
const Injectable = (): ClassDecorator => {
return (target: any) => {
Reflect.defineMetadata('injectable', true, target);
};
};
const Inject = (token: string): ParameterDecorator => {
return (target: any, propertyKey: string | symbol, parameterIndex: number) => {
let existingParameters: string[] = Reflect.getOwnMetadata('parameters', target, propertyKey) || [];
existingParameters[parameterIndex] = token;
Reflect.defineMetadata('parameters', existingParameters, target, propertyKey);
};
};
@Injectable()
class Logger {
log(message: string) {
console.log(`Logger: ${message}`);
}
}
class Greeter {
private logger: Logger;
constructor(@Inject('Logger') logger: Logger) {
this.logger = logger;
}
greet(name: string) {
this.logger.log(`Hello, ${name}!`);
}
}
// Simple dependency injection container
class Container {
private dependencies: Map = new Map();
register(token: string, dependency: any) {
this.dependencies.set(token, dependency);
}
resolve(target: any): T {
const parameters: string[] = Reflect.getMetadata('parameters', target) || [];
const resolvedDependencies = parameters.map(token => this.dependencies.get(token));
return new target(...resolvedDependencies);
}
}
const container = new Container();
container.register('Logger', new Logger());
const greeter = container.resolve(Greeter);
greeter.greet('World'); // Output: Logger: Hello, World!
Šiame pavyzdyje Inject dekoratorius naudojamas priklausomybėms injektuoti į Greeter klasės konstruktorių. Dekoratorius susieja žetoną su parametru, kuris vėliau gali būti naudojamas priklausomybei išspręsti naudojant priklausomybių injekcijos konteinerį. Šis pavyzdys parodo pagrindinį priklausomybių injekcijos įgyvendinimą naudojant dekoratorius ir reflect-metadata biblioteką.
Praktiniai Pavyzdžiai ir Panaudojimo Atvejai
JavaScript dekoratoriai gali būti naudojami įvairiuose scenarijuose siekiant pagerinti kodo kokybę ir supaprastinti kūrimą. Štai keletas praktinių pavyzdžių ir panaudojimo atvejų:
Registravimas ir Auditas
Dekoratoriai gali būti naudojami automatiniam metodų iškvietimų, argumentų ir grąžinamų verčių registravimui, suteikiant vertingų įžvalgų apie programos elgseną ir našumą. Tai gali būti ypač naudinga derinant ir šalinant problemas.
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const startTime = performance.now();
console.log(`[${new Date().toISOString()}] Calling method: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
const endTime = performance.now();
const executionTime = endTime - startTime;
console.log(`[${new Date().toISOString()}] Method ${propertyKey} returned: ${result}. Execution time: ${executionTime.toFixed(2)}ms`);
return result;
};
return descriptor;
}
class ExampleClass {
@LogMethod
complexOperation(a: number, b: number): number {
// Simulate a time-consuming operation
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += a + b + i;
}
return sum;
}
}
const example = new ExampleClass();
example.complexOperation(5, 10);
Šis išplėstinis pavyzdys matuoja metodo vykdymo laiką ir jį registruoja kartu su esama laiko žyma, suteikdamas išsamesnę informaciją našumo analizei.
Autorizacija ir Autentifikacija
Dekoratoriai gali būti naudojami saugumo politikai įgyvendinti, tikrinant vartotojų roles ir leidimus prieš vykdant metodą. Tai gali užkirsti kelią neteisėtai prieigai prie jautrių duomenų ir funkcionalumo.
function Authorize(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const userRole = getCurrentUserRole(); // Function to retrieve the current user's role
if (userRole !== role) {
throw new Error(`Unauthorized: User does not have the required role (${role}) to access this method.`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
function getCurrentUserRole(): string {
// In a real application, this would retrieve the user's role from authentication context
return 'admin'; // Example: Hardcoded role for demonstration
}
class AdminPanel {
@Authorize('admin')
deleteUser(userId: number) {
console.log(`User ${userId} deleted successfully.`);
}
@Authorize('editor')
editArticle(articleId: number) {
console.log(`Article ${articleId} edited successfully.`);
}
}
const adminPanel = new AdminPanel();
try {
adminPanel.deleteUser(123);
adminPanel.editArticle(456); // This will throw an error because the user role is 'admin'
} catch (error) {
console.error(error.message);
}
Šiame išplėstiniame pavyzdyje Authorize dekoratorius patikrina, ar dabartinis vartotojas turi nurodytą rolę, prieš leisdamas pasiekti metodą. getCurrentUserRole funkcija (kuri realioje programoje gautų faktinę vartotojo rolę) naudojama nustatyti dabartinę vartotojo rolę. Jei vartotojas neturi reikiamos rolės, išmetama klaida, neleidžianti metodui įvykdyti.
Kaupimas (Caching)
Dekoratoriai gali būti naudojami brangių operacijų rezultatams kaupti, gerinant programos našumą ir mažinant serverio apkrovą. Tai gali būti ypač naudinga dažnai naudojamiems duomenims, kurie retai keičiasi.
function Cache(ttl: number = 60) { // ttl in seconds, default to 60 seconds
const cache = new Map();
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cacheKey = `${propertyKey}-${JSON.stringify(args)}`;
const cachedData = cache.get(cacheKey);
if (cachedData && Date.now() < cachedData.expiry) {
console.log(`Retrieving from cache: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
return cachedData.data;
}
console.log(`Executing and caching: ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = await originalMethod.apply(this, args);
cache.set(cacheKey, {
data: result,
expiry: Date.now() + ttl * 1000, // Calculate expiry time
});
return result;
};
return descriptor;
};
}
class DataService {
@Cache(120) // Cache for 120 seconds
async fetchData(id: number): Promise {
// Simulate fetching data from a database or API
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Data for ID ${id} fetched from source.`);
}, 1000); // Simulate a 1-second delay
});
}
}
const dataService = new DataService();
(async () => {
console.log(await dataService.fetchData(1)); // Executes the method
console.log(await dataService.fetchData(1)); // Retrieves from cache
await new Promise(resolve => setTimeout(resolve, 121000)); // Wait for 121 seconds to allow the cache to expire
console.log(await dataService.fetchData(1)); // Executes the method again after cache expiry
})();
Šis išplėstinis pavyzdys įgyvendina pagrindinį kaupimo mechanizmą naudojant Map. Cache dekoratorius saugo dekoruoto metodo rezultatus nurodytą laiko tarpą (TTL - time-to-live). Kai metodas vėl iškviečiamas su tais pačiais argumentais, grąžinamas rezultatas iš kaupyklos, o ne vykdomas metodas iš naujo. Pasibaigus TTL, metodas vykdomas vėl, o rezultatas išsaugomas kaupykloje.
Validavimas
Dekoratoriai gali būti naudojami duomenims patikrinti prieš juos apdorojant, užtikrinant duomenų vientisumą ir išvengiant klaidų. Tai gali būti ypač naudinga tikrinant vartotojo įvestį arba duomenis, gautus iš išorinių šaltinių.
function Required() {
return function (target: any, propertyKey: string) {
if (!target.constructor.requiredFields) {
target.constructor.requiredFields = [];
}
target.constructor.requiredFields.push(propertyKey);
};
}
function ValidateClass(target: any) {
const originalConstructor = target;
function construct(constructor: any, args: any[]) {
const instance: any = new constructor(...args);
if (constructor.requiredFields) {
constructor.requiredFields.forEach((field: string) => {
if (!instance[field]) {
throw new Error(`Missing required field: ${field}`);
}
});
}
return instance;
}
const newConstructor: any = function (...args: any[]) {
return construct(originalConstructor, args);
};
newConstructor.prototype = originalConstructor.prototype;
return newConstructor;
}
@ValidateClass
class User {
@Required()
name: string;
@Required()
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
}
try {
const validUser = new User('John Doe', 'john.doe@example.com');
console.log('Valid user created:', validUser);
const invalidUser = new User('Jane Doe', ''); // Missing email
} catch (error) {
console.error('Validation error:', error.message);
}
Šis pavyzdys naudoja du dekoratorius: Required ir ValidateClass. Required dekoratorius pažymi savybes kaip privalomas. ValidateClass dekoratorius perima klasės konstruktorių ir patikrina, ar visi privalomi laukai turi reikšmes. Jei trūksta kurio nors privalomo lauko, išmetama klaida.
Priklausomybių Injekcija
Kaip parodyta parametrų dekoratoriaus pavyzdyje, dekoratoriai gali palengvinti pagrindinę priklausomybių injekciją, todėl lengviau valdyti priklausomybes ir atsieti komponentus. Nors egzistuoja sudėtingesnės priklausomybių injekcijos sistemos, dekoratoriai gali suteikti lengvą ir patogų būdą tvarkyti paprastus priklausomybių injekcijos scenarijus.
Svarstymai ir Geroji Praktika
- Supraskite Vykdymo Kontekstą: Būkite susipažinę su
target,propertyKeyirdescriptorargumentais, perduodamais dekoratoriaus funkcijai. Šie argumentai suteikia vertingos informacijos apie dekoruotą deklaraciją ir leidžia atitinkamai modifikuoti jos elgseną. - Naudokite Dekoratorius Saikingai: Nors dekoratoriai gali būti galingi, perteklinis jų naudojimas gali lemti sudėtingą ir sunkiai suprantamą kodą. Naudokite dekoratorius apgalvotai ir tik tada, kai jie suteikia akivaizdžią naudą kodo pakartotinio naudojimo, skaitomumo ar priežiūros požiūriu.
- Laikykitės Pavadinimų Sutarčių: Naudokite aprašomuosius pavadinimus savo dekoratoriams, kad aiškiai nurodytumėte jų paskirtį. Tai padarys jūsų kodą labiau save dokumentuojantį ir lengviau suprantamą.
- Išlaikykite Atsakomybių Atskyrimą: Dekoratoriai turėtų sutelkti dėmesį į konkrečius kryžminius aspektus ir vengti nesusijusio funkcionalumo maišymo. Tai pagerins jūsų kodo moduliškumą ir priežiūrą.
- Kruopščiai Testuokite Savo Dekoratorius: Kaip ir bet kuris kitas kodas, dekoratoriai turėtų būti kruopščiai testuojami, siekiant užtikrinti, kad jie veiktų teisingai ir nesukeltų nenumatytų šalutinių poveikių.
- Saugokitės Šalutinių Poveikių: Dekoratoriai vykdomi vykdymo metu. Venkite sudėtingų ar ilgai trunkančių operacijų dekoratorių funkcijose, nes tai gali paveikti programos našumą.
- Rekomenduojama Naudoti TypeScript: Nors techniškai JavaScript dekoratorius galima naudoti paprastame JavaScript su „Babel“ transpiliacija, jie dažniausiai naudojami su „TypeScript“. „TypeScript“ suteikia puikų tipų saugumą ir projektavimo laiko patikrinimą dekoratoriams.
Globalios Perspektyvos ir Pavyzdžiai
Kodo pakartotinio naudojimo, priežiūros ir atsakomybių atskyrimo principai, kuriuos palengvina dekoratoriai, yra visuotinai taikomi įvairiuose programinės įrangos kūrimo kontekstuose visame pasaulyje. Tačiau konkretūs įgyvendinimai ir panaudojimo atvejai gali skirtis priklausomai nuo technologijų rinkinio, projekto reikalavimų ir skirtinguose regionuose paplitusių kūrimo praktikų.
Pavyzdžiui, įmonių Java kūrime, anotacijos (koncepciškai panašios į dekoratorius) plačiai naudojamos konfigūracijai ir priklausomybių injekcijai (pvz., „Spring Framework“). Nors sintaksė ir pagrindiniai mechanizmai skiriasi nuo JavaScript dekoratorių, pagrindiniai metaprogramavimo ir AOP principai išlieka tie patys. Panašiai, Python kalboje dekoratoriai yra pirmos klasės kalbos savybė ir dažnai naudojami tokioms užduotims kaip registravimas, autentifikacija ir kaupimas.
Dirbant tarptautinėse komandose arba prisidedant prie atvirojo kodo projektų su pasauline auditorija, būtina laikytis kodavimo standartų ir gerosios praktikos, skatinančios aiškumą ir priežiūrą. Efektyvus dekoratorių naudojimas gali prisidėti prie modularesnės ir gerai struktūrizuotos kodo bazės, todėl skirtingų sričių kūrėjams lengviau bendradarbiauti ir prisidėti.
Išvada
JavaScript dekoratoriai yra galinga ir universali metaprogramavimo funkcija, galinti žymiai pagerinti kodo pakartotinį naudojimą, skaitomumą ir priežiūrą. Suteikdami deklaratyvų būdą pridėti metaduomenis ir įgyvendinti AOP principus, dekoratoriai leidžia inkapsuliuoti bendrą elgseną, atskirti atsakomybes ir kurti modularesnes bei gerai struktūrizuotas programas. Nors tai vis dar aktyviai kuriamas pasiūlymas, dekoratoriai jau rado platų pritaikymą tokiose sistemose kaip „Angular“ ir „NestJS“ ir yra pasirengę tapti vis svarbesne JavaScript ekosistemos dalimi. Suprasdami dekoratorių sintaksę, naudojimą ir gerąją praktiką, galite išnaudoti jų galią kurdami tvirtesnes, lankstesnes ir lengviau prižiūrimas programas.
Kadangi JavaScript ekosistema toliau vystosi, norint kurti aukštos kokybės programinę įrangą, atitinkančią vartotojų poreikius visame pasaulyje, labai svarbu neatsilikti nuo naujų funkcijų ir gerosios praktikos. JavaScript dekoratorių įvaldymas yra vertingas įgūdis, galintis padėti tapti efektyvesniu ir produktyvesniu kūrėju.